/*=============================================================================
    jibits/template.js
    Copyright (C) 2008-2009, Donald W. Griffin
    All rights reserved.
    [MIT license :: see license.txt for details]
=============================================================================*/
$module("jibits.template");

$requires("zjs.text");

$requires("jibits.dom");

$namespace(jibits, {

drillDown : function (inner, outer)
{
    var ret = zjs.chain(inner);
    ret.$outer = outer;
    return ret;
},

/**
This class is used to generate and update from a DOM template. A DOM template is
a tree of DOM nodes used to create duplicate trees. Each duplicate is "fixed" by
replacing the ID's of any elements with a baseid from the top-most element. For
example:

    &lt;div id="foo">
        &lt;div id="bar">&lt;/div>
        &lt;img id="goo">
    &lt;/div>

If the above DOM tree were used to create a new tree with a baseid of "jazz", the
result would be:

    &lt;div id="jazz">
        &lt;div id="jazz_bar">&lt;/div>
        &lt;img id="jazz_goo">
    &lt;/div>

The ID's from the template are called "relative ID's" (often abbreviated "rid").

Beyond ensuring unique ID's for templated elements, this class also provides for
setting any number of attributes on the target tree. This is controlled by the
"template descriptor" object. The template descriptor is supplied along with the
source element tree ID to the constructor and cannot be changed (create a new
instance if desired). For example:

    var tmpl = new jibits.Template("srcid", tdesc);

Each member of the template descriptor is associated with an element and is
named by the element's RELID (relative ID). For example, in the original template,
a template descriptor would look something like this:

    var tdesc =
    {
        bar : { ... },
        goo : { ... }
    }

The "bar" and "goo" properties contain attributes to be applied to the DOM tree.
A common example might be to set the "src" attribute of an IMG element:

    var tdesc =
    {
        bar : { ... },
        goo : { src: "images/pic.png" }
    }

There are also special properties that are understood differently from normal
attributes: $data, $html and $text. The $data property is discussed later, but
the $html and $text properties are simple: $html is used to set the innerHTML
value and the $text property sets the element's text (or value if the element
is an input element). For example:

    var tdesc =
    {
        bar : { $text: "Hello world" },
        goo : { src: "images/pic.png" }
    }

This is adequate for simple replacements, but dynamic content is often needed.
For starters, all strings in a template descriptor are interpreted as a ZJS
text Template (which uses #{prop} syntax). The context object is supplied as
an argument. For example:

    var tdesc =
    {
        bar : { $text: "Hello #{username}" },
        goo : { src: "images/#{userpic}.png" }
    }

The "src" attribute would insert the "userpic" property from the context object.
For even more dynamic content, one can supply a function as the value of any
property. Any such function will be called on-the-fly and given the context
object; it should return the attribute value. For example:

    var tdesc =
    {
        bar : { $text: function (ctx) { return "Hello " + ctx.username; } },
        goo : { src: function (ctx) { return "images/"+ctx.userpic+".png"; } }
    }

The above would achieve the same result as the "#{}" approach. Obviously, where
basic string building is desired, the "#{}" syntax is much cleaner. The option
to create the value via arbitrary code is, however, far more powerful.

When using dynamic values, the data comes from a context object. The initial
context object must be supplied by the user of the template. The $data property,
however, can navigate into the context object to establish the context object
for the dynamic content of each element. For example, assume we have a context
object like this:

    var context = { username: "Bob", profile : { userpic: "Batman" } };

The template descriptor could use $data like so:

    var tdesc =
    {
        bar : { $text: "Hello #{username}" },
        goo : { $data: "profile", src: "images/#{userpic}.png" }
    }

In this case, the context object for the "goo" element would be the "profile"
property of the supplied context object. This is similar to the "with" keyword.
The outer context object can still be accessed with the $outer property of the
context object.

    var tdesc =
    {
        bar : { $text: "Hello #{username}" },
        goo : { $data: "profile", src: "images/#{userpic}.png",
                        title: "#{$outer.username}" }
    }

This kind of sub-context scoping is more common with lists where the context
object is an element of an array, and the $outer object is the object that has
the array property (not the array itself).
*/
Template : $class(function ()
{
    /**
    Complete the template descriptor by reading any missing pieces from the DOM.
    At present, only the elements' text is read from the DOM.
    */
    function complete (el, srcBaseId, desc)
    {
        var id = el.id;
        id = fixBaseId(srcBaseId, id);
        if (!zjs.isEmpty(id))
        {
            if (!(id in desc))
                desc[id] = {};

            if (!("$text" in desc[id]))
            {
                var s = jibits.getText(el);
                if (!zjs.isEmpty(s))
                    desc[id].$text = s;
            }
        }

        for (var c = el.firstChild; c; c = c.nextSibling)
            if (c.nodeType == 1)
                complete(c, srcBaseId, desc);
    }

    /**
    Creates a context object referencing a property of the given context object.
    The returned object uses the actual object as its prototype (meaning it is
    readonly). Also, the $outer property is added to reference the given context
    object.
    */
    function descend (ctx, into)
    {
        var inner = (zjs.getType(into) !== "string") ? into : zjs.deref(ctx, into);
        $assert(inner);

        var ret = jibits.drillDown(inner, ctx);
        return ret;
    }

    function fixBaseId (srcBaseId, id)
    {
        if (zjs.isEmpty(id))
            return "";
        if (zjs.isEmpty(srcBaseId) || !id.startsWith(srcBaseId))
            return id;
        return id.substring(srcBaseId.length);
    }

    function resolve (v, ctx)
    {
        var ret = v, t = zjs.getType(v);
        switch (t)
        {
            case "function":
                ret = v(ctx);
                break;

            case "string":
                ret = zjs.format(v, ctx);
                break;

            case "array":
                ret = new Array(v.length);
                for (var i = v.length; i-- > 0; )
                    ret[i] = resolve(v[i], ctx);
                break;

            case "object":
                ret = {};
                var ctx2 = ctx;
                if (v.$data)
                    ctx2 = descend(ctx, resolve(v.$data, ctx));

                for (var prop in v)
                    if (prop !== "$data")
                        ret[prop] = resolve(v[prop], ctx2);

                break;
        }

        return ret;
    }

    /**
    */
    function update (el, baseid, srcBaseId, desc, ctx)
    {
        var id = el.id;
        if (id && id.startsWith(baseid))
        {
            id = id.substring(baseid.length);
            id = fixBaseId(srcBaseId, id);

            if (id in desc)
            {
                ctx.$element = el.id;
                var attrs = resolve(desc[id], ctx);
                delete ctx.$element;
                jibits.setAttr(el, attrs);
            }
        }

        for (var c = el.firstChild; c; c = c.nextSibling)
            if (c.nodeType == 1)
                update(c, baseid, srcBaseId, desc, ctx);
    }

    return (
    {
        ctor : function (src, desc)
        {
            this.desc = desc || {};
            this.src = $elem(src);
            this.srcBaseId = jibits.Template.getBaseId(src);

            complete(this.src, this.srcBaseId, this.desc);
        },

        applyTo : function (el, ctx)
        {
            if (!ctx)
                return this;

            el = $elem(el);
            update(el, el.id + "_", this.srcBaseId, this.desc, ctx);
            return this;
        },

        generate : function (id, ctx)
        {
            var ret = jibits.cloneElemDeep(this.src, { id : id });

            jibits.addMeta(ret, { baseid : id + "_" });

            this.applyTo(ret, ctx);
            return ret;
        },

        getBaseId : $static(function (el)
        {
            el = $elem(el);
            for (var top = document.body; el && el != top; el = el.parentNode)
            {
                var meta = jibits.getMeta(el, /*noAdd=*/true);
                if (meta && meta.baseid)
                    return meta.baseid;
            }
            return null;
        })
    });
}())

}); // namespace jibits
